import sys
import os
import platform
import struct
import getpass

from made.contrib import compilersettings, compilercommands, tasks
from made.util import command
from made import runtime

from . import libdeps
from . import parsemakefile

# TODO: When calling link_dynamic_library:
#     - pass SESILIB.res in with the object files
#     - get the link libs by running "list_link_libs.py -s -t ds
#       <lib_name>" (add an option that doesn't prepend "-l" or append
#       ".lib")
# TODO: When calling link_binary:
#     - pass SESI.res in with the object files
#     - get the link libs by running "list_link_libs.py -s -t ds
#       <lib_name>" (add an option that doesn't prepend "-l" or append
#       ".lib")
#     - set the subsystem to "console" if the binary's main source contains
#       "main[ \t]*(".  We can call tasks.read_file_now from the binary's
#       madefile to make it dependent on the source file.

_houdini_version = None

def initialize(cpp_compiler_chain=None):
    # Allow sub madefiles to import modules in this directory such as
    # parsemakefile.
    # TODO: This feels like a hack.
    # TODO: Should we use runtime.toplevel_madefile.dir?
    if _current_module_dir() not in sys.path:
        sys.path.append(_current_module_dir())

    # Read the Houdini version from files.  If any of these files change they
    # will trigger the toplevel madefile and all under it to reload.
    global _houdini_version
    _houdini_version = "%s.%s.%s" % (
        tasks.read_file_now("Build/major.release").strip(),
        tasks.read_file_now("Build/minor.release").strip(),
        tasks.read_file_now("Build/build.version").strip())

    hfs = _hfs_dir()
    compilersettings.initialize(
        dynamic_library_output_dir=hfs + (
            "/bin" if platform.system() == "Windows" else "/dsolib"),
        static_library_output_dir="%s/custom/houdini/usolib" % hfs,
        binary_output_dir= "%s/bin" % hfs,
        import_lib_output_dir= (
            "%s/custom/houdini/dsolib" % hfs if platform.system() == "Windows"
            else None),
        version=_houdini_version,
        cpp_compiler_chain=cpp_compiler_chain)

    compilersettings.set_input_dynamic_library_extension_for_cpp_compiler_chain(
        ".a", "msvc")
    compilersettings.set_static_library_extension_for_cpp_compiler_chain(
        ".a", "msvc")

    state = compilersettings.push_state()
    _push_compiler_options(state)
    _push_defines(state)
    _push_include_dirs(state)
    _push_link_dirs(state)
    _push_msvc_default_libs(state)

    # TODO: Create the directories like we do in the sample's madeconfig.
    tasks.install_user_config()
    _install_user_config()

def _install_user_config():
    # TODO: Consider using the decorator used by made.contrib.tasks to indicate
    #       which functions go in user_config.
    cpp_compiler_chain = compilersettings.cpp_compiler_chain
    runtime.add_user_config(
        (("-O3",) if cpp_compiler_chain.name == "gcc"
        else cpp_compiler_chain.default_optimization_options[:]),
        "full_optimization_options")
    runtime.add_user_config(libdeps.input_libs_for_libs)
    runtime.add_user_config(library_from_makefile)
    runtime.add_user_config(subdirs_from_makefile)
    runtime.add_user_config(ensure_file_depends_on_header)
    runtime.add_user_config(vex_rewrite)
    runtime.add_user_config(lex)
    runtime.add_user_config(yacc)

def _current_module_dir():
    return os.path.abspath(os.path.dirname(__file__))

def _hfs_dir():
    return "%s/%s" % (os.path.dirname(runtime.toplevel_madefile().dir), "hfs")

def library_from_makefile(dynamic=True, static=False):
    madefile = runtime.current_madefile()
    makefile_path = madefile.dir + "/Makefile"
    make_vars = parsemakefile.parse_makefile(makefile_path)
    madefile.require_reload_on_file_change(makefile_path)

    source_files = (make_vars.get("CPPFILES", "").split() +
        make_vars.get("CFILES", "").split())
    if platform.system() == "Darwin":
        source_files.extend(make_vars["OBJCPPFILES"].split())

    if not static:
        static = (
            ("COMPILE_STATICLIB_" + _current_library_name())
            in runtime.client_environ)

    runtime.user_config.library(
        _current_library_name(), source_files, dynamic=dynamic, static=static)
    copy_headers(make_vars["EXPORTS"].split())

    # TODO: Run makedepend.  Also add a clean task for Makedepend.

def subdirs_from_makefile():
    madefile = runtime.current_madefile()
    makefile_path = madefile.dir + "/Makefile"
    make_vars = parsemakefile.parse_makefile(makefile_path)
    madefile.require_reload_on_file_change(makefile_path)

    # TODO: Add a big_library_from_makefile rule that calls this function and
    #       generates a task to link a big library.
    subdirs = make_vars["DIRS"].split()
    if ".last" in subdirs:
        subdirs.remove(".last")
    runtime.subdirs(*subdirs)

def copy_headers(headers):
    include_dir = "%s/custom/houdini/include/%s" % (
        _hfs_dir(), _current_library_name())
    runtime.user_config.mkdir(include_dir)

    # Note that we can't just call tasks.copy_files since we want to preserve
    # the timestamps of the headers.
    for header in headers:
        tasks.copy_file(header, "%s/%s" % (include_dir, header))

def vex_rewrite(vrw_file, affected_source_files):
    """Add a task that will generate .h and .C files from .vrw files."""
    file_base_name = os.path.splitext(vrw_file)[0]

    # TODO: We should be using a makefile variable (e.g. PYTHON2.6) instead of
    #       just using "python" here.
    # TODO: Parse C++Defs and make the result available as
    #       runtime.user_config.made_vars
    # TODO: Set the proper preprocessor.  We can use the make variable CPREPROC.
    c_preprocessor="/usr/bin/cpp"

    header_file = file_base_name + ".h"
    task = runtime.user_config.custom_task(
        input_files=(vrw_file,),
        output_files=(file_base_name + ".C", header_file),
        commands=(command.Command(
            "python %s/houdini/bin/utils/vrwcc/vrwcc.py %s %s" % (
                runtime.toplevel_madefile().dir, vrw_file, c_preprocessor)),
        ))

    for affected_source_file in affected_source_files:
        ensure_file_depends_on_header(affected_source_file, header_file)

    return task

def ensure_file_depends_on_header(
        affected_source_file, header_file, build_static_lib=False):
    """Ensure the given C/C++ source file depends on the given generated header
    file.  Otherwise, if we haven't compiled this file before and the .d file
    does not exist, the dependency won't exist and we might try to compile the
    file before running the task that generates the header.
    """
    lib_types = [True]
    if build_static_lib:
        lib_types.append(False)

    for build_dynamic_library in lib_types:
        # Find the task that compiles the source file.
        # TODO: We need to replace backslashes with forward slashes because
        #       abspath adds them in.  We should do this in a single place,
        #       and have a function that does it.  Should
        #       find_task_for_output_file be responsible for calling abspath
        #       and doing the replacement anyway?
        o_file = tasks.source_to_object_file_for_library(
            affected_source_file, _current_library_name(),
            use_dynamic_libraries=build_dynamic_library)
        compile_task = runtime.find_task_for_output_file(
            os.path.abspath(o_file).replace("\\", "/"))
        if compile_task is None:
            raise RuntimeError("Could not find task that generated %s" % o_file)

        # TODO: Create a utility function that does this:
        # TODO: Can we call compile_task.input_files.add?
        header_file_path = os.path.abspath(header_file).replace("\\", "/")
        if not compile_task.input_files.contains(header_file_path):
            compile_task.set_input_files(
                compile_task.input_files.contents + [header_file_path],
                compile_task.files_that_must_exist.contents)

def lex(lex_input_file, cpp_output_file, extra_input_files=()):
    return runtime.user_config.custom_task(
        input_files=(lex_input_file,) + tuple(extra_input_files),
        output_files=(cpp_output_file,),
        commands=(command.Command(
            "flex -o %s %s" % (cpp_output_file, lex_input_file)),
        ))

def yacc(yacc_input_file, header_output_file, cpp_output_file):
    program = ("byacc" if platform.system() == "Windows" else "bison")
    kwargs = dict(
        input_files=(yacc_input_file,),
        output_files=(header_output_file, cpp_output_file),
    )

    if platform.system() != "Windows":
        commands=(
            command.Command(
                "bison -r solved -p vcc %s --defines=%s -o %s" % (
                yacc_input_file, header_output_file, cpp_output_file)),
        )
    else:
        commands=(
            command.Command("byacc -d -p vcc %s" % yacc_input_file),
            _move_file_command("y.tab.c", cpp_output_file),
            _move_file_command("y.tab.h", header_output_file),
        )

    return runtime.user_config.custom_task(commands=commands, **kwargs)

def _move_file_command(from_file, to_file):
    return command.FunctionCall(
        "mv %s %s" % (from_file, to_file),
        "__import__('os').rename", from_file, to_file)

def _current_library_name():
    return os.path.basename(runtime.current_madefile().dir)

def _push_compiler_options(state):
    if compilersettings.cpp_compiler_chain.name == "gcc":
        _push_gcc_compiler_options(state)

    if _is_using_msvc():
        _push_msvc_compiler_options(state)

def _push_gcc_compiler_options(state):
    state.add_extra_options(
        ("-fmessage-length=0", "-Wno-parentheses",
            "-Wno-sign-compare", "-Wno-uninitialized", "-Wunused",
            "-Wno-unused-parameter", "-Werror=sequence-point",
            "-Wno-deprecated"),
        compilercommands.operation.compile)

    state.add_extra_options(
        ("-Woverloaded-virtual", "-Wno-reorder"),
        compilercommands.operation.compile,
        language=compilercommands.language.cpp)

    state.add_extra_options(
        ("-fno-strict-aliasing",),
        compilercommands.operation.compile,
        is_debug=False)

    # Bring the default optimization level down from -O3.
    if compilersettings.cpp_compiler_chain.name == "gcc":
        state.overridden_optimization_options = ("-O2",)

def _push_msvc_compiler_options(state):
    state.defines.append("I386")

    state.add_extra_options((
            "-nologo", # suppress printing compiler info
            "-wd4244", # double to float conversion
            "-wd4305", # double to float conversion
            "-wd4996", # deprecation
            "-wd4355", # this used as initializer to base class
            "-wd4018", # signed/unsigned mismatch
            "-wd4800", # forcing types to bools
            "-wd4345", # simple aggregates and inplace constructors
            "-wd4103", # alignment changes afer including header
            "-w14390", # enable: empy control statement warning
            "-w14101", # enable: unreferenced local variable
            "-we4700", # warning as error: uninitialized local variable
            "-we4717", # warning as error: recursive on all control paths
            "-we4265", # warning as error: destructor is not virtual
        ),
        compilercommands.operation.compile)

    state.add_extra_options((
            "-Zc:forScope", # proper for loop scoping
            "-EHsc",        # use C++ exceptions, but not from extern C's
            "-GR"           # enable RTTI
        ),
        compilercommands.operation.compile,
        language=compilercommands.language.cpp)

    state.add_extra_option(
        "-Z7", # generate C 7.0-compatible debugging info
        compilercommands.operation.compile,
        is_debug=True)

    state.add_extra_options((
            "-STACK:67108864", # 64MB stack size
        ),
        compilercommands.operation.link_binary)

def _push_defines(state):
    state.defines.extend((
        'VERSION="%s"' % _houdini_version,
        'USERNAME="%s"' % getpass.getuser(),
        "ENABLE_UI_THREADS",
        "HAS_PYTHON",
        "BT_USE_DOUBLE_PRECISION",
    ))

    if _is_using_msvc():
        state.defines.extend((
            "WIN32", "_WIN32_WINNT=0x0501", "WINVER=0x0501",
            "_CRT_SECURE_NO_DEPRECATE", "NOMINMAX",
            "NEED_SPECIALIZATION_STORAGE",
            "PNG_USER_CONFIG", "BOOST_IOSTREAMS_NO_LIB"))
    else:
        state.defines.append(
            "LINUX" if platform.system() == "Linux" else "MBSD")
        state.defines.extend(("_GNU_SOURCE", "USE_PTHREADS", "_REENTRANT",
            "_FILE_OFFSET_BITS=64", "GCC3"))
        if compilersettings.cpp_compiler_chain.gcc_version()[0] >= 4:
            state.defines.append("GCC4")

    if struct.calcsize("P") == 8:
        state.defines.extend(("AMD64", "SIZEOF_VOID_P=8"))
    else:
        state.defines.append("SIZEOF_VOID_P=4")

    state.defines.append(
        "SESI_LITTLE_ENDIAN" if sys.byteorder == "little"
        else "SESI_BIG_ENDIAN")

    state.defines.append(lambda is_debug, **kwargs:
        ("UT_ASSERT_LEVEL=2", "UT_DEBUG", "UI_DEBUG")
        if is_debug else ("NDEBUG", "UT_ASSERT_LEVEL=0"))

    state.defines.append(lambda is_debug, **kwargs:
        ("UT_LIBRARY_TYPE=%d" % (
            not is_debug and
            compilersettings.cpp_compiler_chain.name != "msvc"),))

    if _is_using_msvc():
        def get_msvc_export_defines(is_shared_library, library_name, **kwargs):
            if is_shared_library is None:
                return ()
            return (("EXPORT_%s" % library_name if is_shared_library
                else "MAKING_STATIC"),)
        state.defines.append(get_msvc_export_defines)

def _push_include_dirs(state):
    hfs = _hfs_dir()
    state.include_dirs.extend((
        ".",
        "%s/custom/houdini/include" % hfs,
        "%s/custom/include" % hfs,
        "%s/custom/include/zlib" % hfs))

    if platform.system() == "Windows":
        state.include_dirs.extend("%s/%s" % (
                compilersettings.cpp_compiler_chain.msvc_dir, subdir)
            for subdir in ("include", "PlatformSDK/Include"))

def _push_link_dirs(state):
    # TODO: Set link directories based on whether the binary is static
    #       or dynamic.
    if _is_using_msvc():
        # TODO: Use compilersettings.import_lib_output_dir.
        state.link_dirs.append(_hfs_dir() + "/custom/houdini/dsolib")
    else:
        state.link_dirs.append(_hfs_dir() + "/dsolib")
        if platform.system() == "Linux":
            state.link_dirs.extend(("/usr/X11R6/lib64", "/usr/X11R6/lib"))

def _push_msvc_default_libs(state):
    state.msvc_default_libs.extend((
        "MPR", "GLAUX", "OLE32", "COMDLG32", "VERSION", "UUID", "GLU32",
        "GDI32", "WINMM", "VFW32", "ADVAPI32", "WS2_32", "USER32", "NETAPI32",
        "OPENGL32", "SHELL32", "IPHlpAPI", "DelayImp", "COMCTL32",
    ))

def _is_using_msvc():
    return compilersettings.cpp_compiler_chain.name == "msvc"

#-----------------------------------------------------------------------------

# TODO: Use this function.  We currently don't invoke makedepend.
def get_extra_makedepend_defines():
    if not _is_using_msvc():
        return ()

    return ("_MSC_VER=1400", "_MT", "_CPPUNWIND", "_CPPRTTI",
        "WIN32_LEAN_AND_MEAN", "NULL=0" "__RPC_H__", "_M_IX86",
        "PLATFORM_OPEN_GL", "DEFINE_GUID")

